/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 Grigori Goronzy <goronzy@kinoho.net>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>

#include "chachapoly.h"

#include "py/runtime.h"

/**
 * Constant-time memory compare. This should help to protect against
 * side-channel attacks.
 *
 * \param av input 1
 * \param bv input 2
 * \param n bytes to compare
 * \return 0 if inputs are equal
 */
static int memcmp_eq(const void *av, const void *bv, int n)
{
    const unsigned char *a = (const unsigned char*) av;
    const unsigned char *b = (const unsigned char*) bv;
    unsigned char res = 0;
    int i;

    for (i = 0; i < n; i++) {
        res |= *a ^ *b;
        a++;
        b++;
    }

    return res;
}

/**
 * Poly1305 tag generation. This concatenates a string according to the rules
 * outlined in RFC 7539 and calculates the tag.
 *
 * \param poly_key 32 byte secret one-time key for poly1305
 * \param ad associated data
 * \param ad_len associated data length in bytes
 * \param ct ciphertext
 * \param ct_len ciphertext length in bytes
 * \param tag pointer to 16 bytes for tag storage
 */
static void poly1305_get_tag(unsigned char *poly_key, const void *ad,
        int ad_len, const void *ct, int ct_len, unsigned char *tag)
{
    struct poly1305_context poly;
    unsigned left_over;
    uint64_t len;
    unsigned char pad[16];

    poly1305_init(&poly, poly_key);
    memset(&pad, 0, sizeof(pad)); 

    /* associated data and padding */
    poly1305_update(&poly, ad, ad_len);
    left_over = ad_len % 16;
    if (left_over)
        poly1305_update(&poly, pad, 16 - left_over);

    /* payload and padding */
    poly1305_update(&poly, ct, ct_len);
    left_over = ct_len % 16;
    if (left_over)
        poly1305_update(&poly, pad, 16 - left_over);
    
    /* lengths */
    len = ad_len;
    poly1305_update(&poly, (unsigned char *)&len, 8);
    len = ct_len;
    poly1305_update(&poly, (unsigned char *)&len, 8);

    poly1305_finish(&poly, tag);
}

int chachapoly_init(struct chachapoly_ctx *ctx, const void *key, int key_len)
{
    assert (key_len == 128 || key_len == 256);

    memset(ctx, 0, sizeof(*ctx));
    chacha_keysetup(&ctx->cha_ctx, key, key_len);
    return CHACHAPOLY_OK;
}

int chachapoly_crypt(struct chachapoly_ctx *ctx, const void *nonce,
        const void *ad, int ad_len, void *input, int input_len,
        void *output, void *tag, int tag_len, int encrypt)
{
    unsigned char poly_key[CHACHA_BLOCKLEN];
    unsigned char calc_tag[POLY1305_TAGLEN];
    const unsigned char one[4] = { 1, 0, 0, 0 };

    /* initialize keystream and generate poly1305 key */
    memset(poly_key, 0, sizeof(poly_key));
    chacha_ivsetup(&ctx->cha_ctx, nonce, NULL);
    chacha_encrypt_bytes(&ctx->cha_ctx, poly_key, poly_key, sizeof(poly_key));

    /* check tag if decrypting */
    if (encrypt == 0 && tag_len) {
        poly1305_get_tag(poly_key, ad, ad_len, input, input_len, calc_tag);
        if (memcmp_eq(calc_tag, tag, tag_len) != 0) {
            return CHACHAPOLY_INVALID_MAC;
        }
    }

    /* crypt data */
    chacha_ivsetup(&ctx->cha_ctx, nonce, one);
    chacha_encrypt_bytes(&ctx->cha_ctx, (unsigned char *)input,
                         (unsigned char *)output, input_len);

    /* add tag if encrypting */
    if (encrypt && tag_len) {
        poly1305_get_tag(poly_key, ad, ad_len, output, input_len, calc_tag);
        memcpy(tag, calc_tag, tag_len);
    }

    return CHACHAPOLY_OK;
}

int chachapoly_crypt_short(struct chachapoly_ctx *ctx, const void *nonce,
        const void *ad, int ad_len, void *input, int input_len,
        void *output, void *tag, int tag_len, int encrypt)
{
    unsigned char keystream[CHACHA_BLOCKLEN];
    unsigned char calc_tag[POLY1305_TAGLEN];
    int i;

    assert(input_len <= 32);

    /* initialize keystream and generate poly1305 key */
    memset(keystream, 0, sizeof(keystream));
    chacha_ivsetup(&ctx->cha_ctx, nonce, NULL);
    chacha_encrypt_bytes(&ctx->cha_ctx, keystream, keystream,
            sizeof(keystream));

    /* check tag if decrypting */
    if (encrypt == 0 && tag_len) {
        poly1305_get_tag(keystream, ad, ad_len, input, input_len, calc_tag);
        if (memcmp_eq(calc_tag, tag, tag_len) != 0) {
            return CHACHAPOLY_INVALID_MAC;
        }
    }

    /* crypt data */
    for (i = 0; i < input_len; i++) {
        ((unsigned char *)output)[i] =
            ((unsigned char *)input)[i] ^ keystream[32 + i];
    }

    /* add tag if encrypting */
    if (encrypt && tag_len) {
        poly1305_get_tag(keystream, ad, ad_len, output, input_len, calc_tag);
        memcpy(tag, calc_tag, tag_len);
    }

    return CHACHAPOLY_OK;
}

// 当前只支持一个实例
struct chachapoly_ctx	aead_ctx;
uint8_t					ad_ctx[16];
uint8_t					ad_len;

STATIC mp_obj_t aead_chachapoly_init(mp_obj_t key_obj, mp_obj_t ad_obj) {
    // Key 256bit
	mp_buffer_info_t key;
	mp_buffer_info_t ad;

    mp_get_buffer_raise(key_obj, &key, MP_BUFFER_READ);

	if ((key.len != 32) && (key.len != 16)) {
		#if MICROPY_ERROR_REPORTING == MICROPY_ERROR_REPORTING_TERSE
        mp_raise_ValueError(MP_ERROR_TEXT("Key has wrong length."));
        #else
        mp_raise_msg_varg(&mp_type_ValueError,
            MP_ERROR_TEXT("requested length 16 or 32 bytes but object has length %d"), key.len);
        #endif
	}

	mp_get_buffer_raise(ad_obj, &ad, MP_BUFFER_READ);
	if (ad.len > 16) {
		#if MICROPY_ERROR_REPORTING == MICROPY_ERROR_REPORTING_TERSE
        mp_raise_ValueError(MP_ERROR_TEXT("ad too long."));
        #else
        mp_raise_msg_varg(&mp_type_ValueError,
            MP_ERROR_TEXT("requested length <= 16 bytes but object has length %d"), ad.len);
        #endif	
	}

	chachapoly_init(&aead_ctx, key.buf, key.len * 8);
	ad_len = ad.len;
	memcpy(ad_ctx, ad.buf, ad_len);

    return mp_const_none;
    // return mp_obj_new_bytes(key.buf, key.len);
}

STATIC MP_DEFINE_CONST_FUN_OBJ_2(aead_chachapoly_init_obj, aead_chachapoly_init);

STATIC mp_obj_t aead_chachapoly_encrypt(mp_obj_t buf_obj, mp_obj_t random_obj, mp_obj_t tag_obj) {
	mp_buffer_info_t	buf;
	mp_buffer_info_t	random;
	mp_buffer_info_t	tag;

	mp_get_buffer_raise(buf_obj, &buf, MP_BUFFER_RW);
	mp_get_buffer_raise(random_obj, &random, MP_BUFFER_READ);
	mp_get_buffer_raise(tag_obj, &tag, MP_BUFFER_WRITE);

	// check random
	if (random.len != 8) {
		mp_raise_ValueError(MP_ERROR_TEXT("random wrong length."));
	}
	// check tag size(16bytes)
	if (tag.len != 16) {
		mp_raise_ValueError(MP_ERROR_TEXT("tag wrong length."));
	}

	chachapoly_crypt(&aead_ctx, random.buf, ad_ctx, ad_len, buf.buf, buf.len, buf.buf, tag.buf, tag.len, 1);

	return mp_const_none;
}

STATIC MP_DEFINE_CONST_FUN_OBJ_3(aead_chachapoly_encrypt_obj, aead_chachapoly_encrypt);

STATIC mp_obj_t aead_chachapoly_decrypt(mp_obj_t buf_obj, mp_obj_t random_obj, mp_obj_t tag_obj) {
	mp_buffer_info_t	buf;
	mp_buffer_info_t	random;
	mp_buffer_info_t	tag;

	mp_get_buffer_raise(buf_obj, &buf, MP_BUFFER_RW);
	mp_get_buffer_raise(random_obj, &random, MP_BUFFER_READ);
	mp_get_buffer_raise(tag_obj, &tag, MP_BUFFER_READ);

	// check random
	if (random.len != 8) {
		mp_raise_ValueError(MP_ERROR_TEXT("random wrong length."));
	}
	// check tag size(16bytes)
	if (tag.len != 16) {
		mp_raise_ValueError(MP_ERROR_TEXT("tag wrong length."));
	}

	return mp_obj_new_bool(CHACHAPOLY_OK == chachapoly_crypt(&aead_ctx, random.buf, ad_ctx, ad_len, buf.buf, buf.len, buf.buf, tag.buf, tag.len, 0));
}

STATIC MP_DEFINE_CONST_FUN_OBJ_3(aead_chachapoly_decrypt_obj, aead_chachapoly_decrypt);


STATIC const mp_rom_map_elem_t aead_module_globals_table[] = {
    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_aead) },
    { MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&aead_chachapoly_init_obj) },
	{ MP_ROM_QSTR(MP_QSTR_encrypt), MP_ROM_PTR(&aead_chachapoly_encrypt_obj) },
	{ MP_ROM_QSTR(MP_QSTR_decrypt), MP_ROM_PTR(&aead_chachapoly_decrypt_obj) },
};
STATIC MP_DEFINE_CONST_DICT(aead_module_globals, aead_module_globals_table);

// Define module object.
const mp_obj_module_t aead_user_cmodule = {
    .base = { &mp_type_module },
    .globals = (mp_obj_dict_t *)&aead_module_globals,
};

// Register the module to make it available in Python.
MP_REGISTER_MODULE(MP_QSTR_aead, aead_user_cmodule, MODULE_AEAD_ENABLED);
